import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@supabase/supabase-js'
// Create server-side Supabase client with user session
function createServerSupabaseClient(request: Request) {
// Get the authorization header from the request
const authHeader = request.headers.get('authorization')
return createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
auth: {
autoRefreshToken: false,
persistSession: false
},
global: {
headers: authHeader ? {
Authorization: authHeader
} : {}
}
}
)
}
export async function POST(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const supabase = createServerSupabaseClient(request)
const bookId = params.id
const { message, changes } = await request.json()
if (!message || !changes || changes.length === 0) {
return NextResponse.json(
{ error: 'Commit message and changes are required' },
{ status: 400 }
)
}
// Get current user
const { data: { user }, error: authError } = await supabase.auth.getUser()
if (authError || !user) {
return NextResponse.json(
{ error: 'Authentication required' },
{ status: 401 }
)
}
// Get GitHub integration
const { data: profile } = await supabase
.from('profiles')
.select('github_integrations')
.eq('id', user.id)
.single()
const integration = profile?.github_integrations?.[bookId]
if (!integration) {
return NextResponse.json(
{ error: 'GitHub integration not found' },
{ status: 404 }
)
}
// Get book files to create file content for commit
const { data: files } = await supabase
.from('file_system_items')
.select('*')
.eq('book_id', bookId)
if (!files) {
return NextResponse.json(
{ error: 'No files found' },
{ status: 404 }
)
}
// Create file contents for GitHub commit
const fileContents: { [path: string]: string } = {}
// Build file structure
const buildFilePath = (fileId: string, allFiles: any[]): string => {
const file = allFiles.find(f => f.id === fileId)
if (!file) return ''
// For files, ensure extension is included
let fileName = file.name
if (file.type === 'file' && file.file_extension && !fileName.includes('.')) {
fileName = `${fileName}.${file.file_extension}`
}
if (!file.parent_id) return fileName
const parentPath = buildFilePath(file.parent_id, allFiles)
return parentPath ? `${parentPath}/${fileName}` : fileName
}
// Add all files to commit
files.forEach(file => {
if (file.type === 'file') {
const filePath = buildFilePath(file.id, files)
fileContents[filePath] = file.content || ''
}
})
// Commit via GitHub API
const owner = integration.github_username
const repo = integration.repository_name
const accessToken = integration.access_token
// Get current branch reference
const branchResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/main`, {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Accept': 'application/vnd.github.v3+json'
}
})
if (!branchResponse.ok) {
throw new Error('Failed to get branch reference')
}
const branchData = await branchResponse.json()
const parentSha = branchData.object.sha
// Get current tree
const treeResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees/${parentSha}`, {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Accept': 'application/vnd.github.v3+json'
}
})
if (!treeResponse.ok) {
throw new Error('Failed to get current tree')
}
// Create blobs and tree entries
const treeEntries = await Promise.all(
Object.entries(fileContents).map(async ([path, content]) => {
const blobResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/blobs`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Accept': 'application/vnd.github.v3+json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
content: Buffer.from(content).toString('base64'),
encoding: 'base64'
})
})
if (!blobResponse.ok) {
throw new Error(`Failed to create blob for ${path}`)
}
const blob = await blobResponse.json()
return {
path,
mode: '100644',
type: 'blob',
sha: blob.sha
}
})
)
// Create new tree
const newTreeResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Accept': 'application/vnd.github.v3+json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
base_tree: parentSha,
tree: treeEntries
})
})
if (!newTreeResponse.ok) {
throw new Error('Failed to create tree')
}
const newTree = await newTreeResponse.json()
// Create commit
const commitResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/commits`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Accept': 'application/vnd.github.v3+json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
message,
tree: newTree.sha,
parents: [parentSha],
author: {
name: user.user_metadata?.full_name || user.email?.split('@')[0] || 'BookWiz User',
email: user.email,
date: new Date().toISOString()
}
})
})
if (!commitResponse.ok) {
throw new Error('Failed to create commit')
}
const commit = await commitResponse.json()
// Update branch reference
const updateRefResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/main`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Accept': 'application/vnd.github.v3+json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
sha: commit.sha
})
})
if (!updateRefResponse.ok) {
throw new Error('Failed to update branch reference')
}
return NextResponse.json({
success: true,
commit: {
sha: commit.sha,
message: commit.message,
url: commit.html_url
}
})
} catch (error) {
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to commit changes' },
{ status: 500 }
)
}
}